package badugi;

import java.util.ArrayList;
import java.util.Random;

/**
 * Server-side class. Table would rule the game.
 * @author agurgul
 *
 */
public class Table {
	/**
	 * The sum of all bids on the table.
	 */
	private int pot = 0;

	/**
	 * Indicates, whether the game is on.
	 */
	private boolean game = false;

	/**
	 * The server associated with this table.
	 */
	private Server server;

	/**
	 * Indicates, which round it is.
	 */
	private int round;

	/**
	 * Indicates, how many players are still in game (excluding the ones who have folded).
	 */
	private int playersInGame;

	/**
	 * Indicates, which player has the dealer button.
	 */
	private int dealerButton = -1;

	/**
	 * Indicates, whether players can change cards.
	 */
	private boolean changingCards;

	/**
	 * Tells, which player's turn it is.
	 */
	private int playerTurn;

	/**
	 * Tells, which player starts this round.
	 */
	private int startPlayer;

	/**
	 * {@link Deck} of {@link Card}s
	 */
	private Deck deck;

	/**
	 * Amount of chips that each player would receive after connecting to the server
	 */
	private int chips;

	public int getChips() {
		return chips;
	}

	/**
	 * Parameter of Badugi game. Defines the value of the small blind
	 */
	private int smallBlind;

	public int getSmallBlind() {
		return smallBlind;
	}

	/**
	 * Parameter of Badugi game. Defines the value of the big blind
	 */
	private int bigBlind;

	public int getBigBlind() {
		return bigBlind;
	}

	/**
	 * The highest bet made so far.
	 */
	private int highestBet = 0;

	/**
	 * An array that stores references to players.
	 */
	private ArrayList<Player> players;

	public ArrayList<Player> getPlayers() {
		return players;
	}

	/**
	 * Due to the fact, that the {@link Table} should know what {@link Card} which player has it stores it in this data in this array.
	 */
	private ArrayList<Card[]> playersCards;

	/**
	 * Shows how many players have reported being ready to play.
	 */
	private boolean[] playersReady;

	/**
	 * Contains bets all of players.
	 */
	private int[] playerBets;

	/**
	 * Creates a new Table connected with the given server.
	 * @param server the server associated with this table.
	 */
	public Table( Server server, int smallBlind, int bigBlind, int chips ) {
		this.server = server;
		this.smallBlind = smallBlind;
		this.bigBlind = bigBlind;
		this.chips = chips;
		deck = new Deck();
		playerBets = new int[ 6 ];
		for( int i = 0; i < playerBets.length; i++ )
			playerBets[ i ] = -1;
		playersReady = new boolean[ 6 ];
		for( int i = 0; i < playersReady.length; i++ )
			playersReady[ i ] = false;
		players = new ArrayList<Player>( 6 );
		playersCards = new ArrayList<Card[]>( 6 );
	}

	/**
	 * Adds a player after he connects with the server.
	 */
	public void addPlayer( int id ) {
		Player player = new Player( id );
		if( id >= players.size() ) {
			players.add( player );
			playersCards.add( player.getCards() );

		}
		else
			for( int i = 0; i < players.size(); i++ ) {
				if( players.get( i ) == null ) {
					players.set( i, player );
					playersCards.set( i, player.getCards() );
				}
			}
		player.setBalance( chips );
	}

	/**
	 * Checks, whether the player with the given ID can make a move.
	 * @param id ID of a player
	 * @return true, when it's the player's turn and false, when it's not.
	 */
	private boolean isLegitMove( int id ) {
		return playerTurn == id;
	}

	private void forcibleFold( Player player ) {
		sendMoveMessage( "F", player.getID() );
		fold( player );
	}

	/**
	 * Method invoked when a player calls. If he tries to cheat, he automatically folds.
	 * When he doesn't have enough money to call, he plays all-in.
	 * @param player the player that calls.
	 */
	private void call( Player player ) {
		playerTurn = ( playerTurn + 1 ) % players.size();
		int balance = player.getBalance();
		int bid = player.getBid();
		int diff = highestBet - bid;
		if( diff <= 0 )
			forcibleFold( player );
		if( balance - diff > 0 ) {
			player.setBalance( balance - diff );
			player.setBid( highestBet );
		}
		else {
			player.setBid( bid + balance );
			player.setBalance( 0 );
			player.setAllIn( true );
		}
		pot += player.getBid() - bid;
		playerBets[ player.getID() ] = player.getBid();
	}

	/**
	 * Method invoked when a player checks. If he's cheating, he folds.
	 * @param player the player that checks.
	 */
	private void check( Player player ) {
		playerTurn = ( playerTurn + 1 ) % players.size();
		if( highestBet != player.getBid() )
			forcibleFold( player );
	}

	/**
	 * Invoked when a player raises or bets. When he tries to cheat, he folds.
	 * When he doesn't have enough money, he plays all-in.
	 * @param player the player that raises or bets.
	 * @param bet the amount of money that is being bet.
	 */
	private void raise( Player player, int bet, boolean blind ) {
		if( !blind )
			playerTurn = ( playerTurn + 1 ) % players.size();

		int balance = player.getBalance();
		int bid = player.getBid();
		int diff = highestBet - bid;
		if( balance - diff <= 0 ) {
			player.setAllIn( true );
			player.setBid( bid + balance );
			player.setBalance( 0 );
			pot += balance;
		}
		else if( balance - bet < 0 )
			forcibleFold( player );
		else {
			if( bid + bet <= highestBet )
				forcibleFold( player );
			else {
				player.setBid( bid + bet );
				player.setBalance( balance - bet );
				highestBet = bid + bet;
			}
			pot += player.getBid() - bid;
		}
		playerBets[ player.getID() ] = player.getBid();
	}

	/**
	 * Invoked when a player folds.
	 * @param player the player that folds.
	 */
	private void fold( Player player ) {
		playerTurn = ( playerTurn + 1 ) % players.size();
		player.setFolded( true );
		playersInGame--;
	}

	/**
	 * Method for checking, whether a player can bet a blind and playing it.
	 * When a player doesn't have enough money, he loses.
	 * @param blind big blind or small blind.
	 * @param player the player that bets a blind.
	 */
	private void betBlind( int blind, Player player ) {
		if( blind > player.getBalance() ) {
			forcibleFold( player );
			player.setLost( true );
			return;
		}
		receiveMessage( "RB" + blind, player.getID() );
	}

	/**
	 * Readies the table before the game.
	 */
	private void readyTheTable() {
		playersInGame = players.size();
		System.out.println( "Players in game:" + playersInGame );
		Random random = new Random();
		if( dealerButton == -1 )
			dealerButton = random.nextInt( players.size() );
		else
			dealerButton = ( dealerButton + 1 ) % playersInGame;
		startPlayer = ( dealerButton + 3 ) % playersInGame;
		playerTurn = startPlayer;
		round = 1;
		changingCards = false;
		server.sendSetUpInfo( dealerButton );
		for( int i = 0; i < players.size(); i++ ) {
			Player player = players.get( i );
			if( player != null ) {
				player.setBid( 0 );
				for( int j = 0; j < player.getCards().length; j++ ) {
					Card card = deck.drawCard();
					player.setCard( card, j );
				}
				server.sendCardInfo( false, player.getID() );
			}
		}
		betBlind( smallBlind, players.get( ( dealerButton + 1 ) % playersInGame ) );
		betBlind( bigBlind, players.get( ( dealerButton + 2 ) % playersInGame ) );
		server.sendTurnInfo( startPlayer, "B" + determineMoves( players.get( startPlayer ) ) );
	}

	/**
	 * Chooses the starting player, plays blinds, checks when the game ends and then sends info about winners.
	 */
	private void play() {
		for( int i = 0; i < playerBets.length; i++ ) {
			if( i < players.size() && players.get( i ) != null )
				playerBets[ i ] = 0;
			else
				playerBets[ i ] = -1;
		}

		while( round < 8 && playersInGame > 1 ) {
			if( players.get( startPlayer ).isFolded() )
				startPlayer = ( startPlayer + 1 ) % playersInGame;
			if( allEqual() && playerTurn == lastPlayer() ) {
				if( round > 2 ) {
					startPlayer = ( dealerButton + 1 ) % playersInGame;
				}
			}
			boolean shouldEnd = true;
			for( Player player : players ) {
				if( !player.isLost() && !player.isAllIn() && !player.isFolded() )
					shouldEnd = false;
			}
			if( shouldEnd )
				break;
		}

		ArrayList<Player> winners = chooseWinners();
		server.sendCardInfo( true, -1 );
		sendWinInfo( winners, dividePot( winners ) );
		for( int i = 0; i < playersReady.length; i++ ) {
			playersReady[ i ] = false;
			playerBets[ i ] = -1;
		}

		readyTheTable();
	}

	/**
	 * Sends info about winners via server.
	 * @param winners the players who won.
	 * @param winnings the amounts of money won by every player.
	 */
	private void sendWinInfo( ArrayList<Player> winners, int[] winnings ) {
		for( int i = 0; i < players.size(); i++ ) {
			int balance = players.get( i ).getBalance();
			server.sendWinInfo( i, winnings[ i ], balance );
		}
	}

	/**
	 * Puts players in order based on their hands' strength. Doesn't include the ones who have folded or lost.
	 * Stops after adding the best not all-in player.
	 * @return the player with the winning hand.
	 */
	private ArrayList<Player> chooseWinners() {
		ArrayList<Player> winners = new ArrayList<Player>( 6 );
		ArrayList<Player> players = new ArrayList<Player>( this.players );

		for( Player player : players ) {
			if( player.isLost() || player.isFolded() )
				players.remove( player );
		}

		Player winner = players.get( 0 );
		int bid = 0;
		if( playersInGame > 1 ) {
			while( bid < highestBet ) {
				for( int i = 1; i < playersCards.size(); i++ ) {
					Card[] winningHand = HandEvaluation.evaluateHands( winner.getCards(), playersCards.get( i ) );
					for( Player player : players ) {
						if( player.getCards() == winningHand ) {
							winner = player;
							break;
						}
					}
				}
				players.remove( winner );
				winners.add( winner );
				bid = winner.getBid();
			}
		}
		else
			winners = players;

		return winners;
	}

	/**
	 * Divides the pot between players, depending on their bets and sends info about it to the players.
	 * @param winners the players who get the winnings.
	 */
	private int[] dividePot( ArrayList< Player > winners ) {
		int[] allWinnings = new int[ 6 ];
		for( int i = 0; i < allWinnings.length; i++ ) {
			allWinnings[ i ] = 0;
		}

		if( winners.size() == 1 ) {
			Player winner = winners.get( 0 );
			winner.setBalance( winner.getBalance() + pot );
			allWinnings[ players.indexOf( winner ) ] = pot;
			pot = 0;
		}
		else {
			for( int i = 0; i < winners.size() && pot > 0; i++ ) {
				Player winner = winners.get( i );
				if( HandEvaluation.evaluateHands( winner.getCards(), winners.get( i + 1 ).getCards() ).length == 0 )
					pot = 0;
				if( !winner.isAllIn() ) {
					winner.setBalance( winner.getBalance() + pot );
					pot = 0;
				} else {
					int winnings = 0;
					for( Player player : players ) {
						winnings += Math.min( player.getBid(), winner.getBid() );
					}
					winner.setBalance( winner.getBalance() + winnings );
					pot -= winnings;
					allWinnings[ players.indexOf( winner ) ] = winnings;
				}
			}
		}
		for( int i = 0; i < players.size(); i++ )
			players.get( i ).setBalance( players.get( i ).getBalance() + allWinnings[ i ] );

		return allWinnings;
	}

	/**
	 * Changes a card of ta player, putting the old card on the used cards deck and drawing a bew one from the main deck.
	 * @param id Player whose card will be changed
	 * @param cardSlot slot on which a change will be made
	 */
	private void changeCard( int id, int cardSlot ) {
		Card newCard = deck.drawCard();
		deck.getUsedCards().add( playersCards.get( id )[ cardSlot ] ); //W RAZIE BLEDOW, TU MOGA NIE PYKAC INDEKSY
		players.get( id ).setCard( newCard, cardSlot );
		playersCards.get( id )[ cardSlot ] = newCard;
		HandEvaluation.sortCards( players.get( id ).getCards() );
		HandEvaluation.sortCards( playersCards.get( id ) );
	}

	/**
	 * Finds the last player.
	 * @return the ID of the last player.
	 */
	private int lastPlayer() {
		int size = players.size();
		return  ( ( ( startPlayer - 1 ) % size ) + size ) % size;
	}

	/**
	 * Checks, if all bets are equal.
	 * @return true when all bets are equal, false otherwise.
	 */
	private boolean allEqual() {
		for( int i = 0; i < players.size(); i++ ) {
				Player player = players.get( i );
				if( highestBet != player.getBid() && !player.isAllIn() && !player.isFolded() && !player.isLost() )
					return false;
		}
		return true;
	}

	private void notYourTurn( int id ) {
		server.sendMessage( "Not your turn.", id );
	}

	/**
	 * Receives a message from a player about his move, performs the move and sends info about it to all players.
	 * @param message the message sent by the player.
	 * @param id the ID of the player.
	 */
	public void receiveMessage( String message, int id ) {
		if( playersInGame == 1 )
			return;
		Player player = players.get( id );
		if( player.isFolded() || player.isAllIn() || player.isLost() ) {
			playerTurn = ( playerTurn + 1 ) % players.size();
			sendTurnMessage( message, id );
			return;
		}
		int cards = message.length();
		boolean legitMove = isLegitMove( id );
		if( !changingCards ) {
			switch( message.charAt( 0 ) ) {
				case 'F':
					if( legitMove )
						fold( player );
					else
						notYourTurn( id );
					break;
				case 'H':
					if( legitMove )
						check( player );
					else
						notYourTurn( id );
					break;
				case 'C':
					if( legitMove )
						call( player );
					else
						notYourTurn( id );
					break;
				case 'R':
					boolean blind = message.charAt( 1 ) == 'B';
					if( legitMove || blind ) {
						try {
							String betString = blind ? message.substring( 2 ) : message.substring( 1 );
							int bet = Integer.parseInt( betString );
							System.out.println( "Raise: " + bet );
							raise( player, bet, blind );
						} catch( NumberFormatException e ) {
							server.playerWorkers.get( id ).getOut().println( "Wrong message." );
						}
					}
					else
						notYourTurn( id );
					break;
				case 'S':
					if( game || playersReady[ id ] )
						return;
					playersReady[ id ] = true;
					int playersReady = 0;
					for( boolean ready : this.playersReady )
						if( ready )
							playersReady++;
					if( playersReady == players.size() && players.size() > 1 ) {
						game = true;
						readyTheTable();
						new Thread( new Runnable() {
							@Override
							public void run() {
								play();
							}
						} ).start();
					}
					return;
				case 'Q':
					players.set( id, null );
					playersCards.set( id, null );
					break;
				default:
					server.playerWorkers.get( id ).getOut().println( "Wrong message about betting." );
			}
		}
		else {
			if( !message.equals( "-1" ) ) {
				for( int i = 0; i < cards; i++ ) {
					try {
						int card = Integer.parseInt( message.substring( i, i + 1 ) );
						if( card < 4 && card >= 0 )
							changeCard( id, card );
					} catch( NumberFormatException e ) {
						server.playerWorkers.get( id ).getOut().println( "Wrong message about cards." );
					}
				}
			}
			server.sendCardInfo( false, id );
		}

		sendMoveMessage( message, id );
		if( message.length() > 2 && message.charAt( 1 ) == 'B' ) {
			try {
				if( Integer.parseInt( message.substring( 2 ) ) == smallBlind )
					return;
			} catch( NumberFormatException e ) {
				System.out.println( "Wrong format" );
			}
		}
		if( playersInGame < 2 )
			return;
		sendTurnMessage( message, id );
	}

	public void sendMoveMessage( String message, int id ) {
		Player player = players.get( id );
		int balance;
		if( !player.isFolded() )
			balance = players.get( id ).getBalance();
		else
			balance = 0;
		char action = message.charAt( 0 );
		if( !Character.isDigit( action ) && action != '-' ) {
			if( playerTurn == startPlayer && allEqual() ) {
				server.sendMoveInfo( id, changingCards, -1, message.charAt( 0 ), player.getBid(), balance );
				System.out.println( "Bet sent: " + player.getBid() );
			}
			else
				server.sendMoveInfo( id, changingCards, -1, message.charAt( 0 ), player.getBid(), balance );
		}
		else {
			int cardsChanged;
			if( message.equals( "-1" ) )
				cardsChanged = 0;
			else
				cardsChanged = message.length();
			if( playerTurn == startPlayer )
				server.sendMoveInfo( id, changingCards, cardsChanged, 'K', -1, -1 );
			else
				server.sendMoveInfo( id, changingCards, cardsChanged, 'K', -1, -1 );
		}
	}

	private String determineMoves( Player player ) {
		int[] moves = { 1, 1, 1, 1, 1 };
		if( player.isAllIn() || player.isLost() || player.isFolded() )
			for( int move : moves )
				move = 0;
		if( highestBet - player.getBid() <= 0 )
			moves[ 2 ] = 0;
		if( highestBet != player.getBid() )
			moves[ 1 ] = 0;
		StringBuilder sb = new StringBuilder( 5 );
		for( int move : moves )
			sb.append( move );
		return new String( sb );
	}

	public void sendTurnMessage( String message, int id ) {
		char action = message.charAt( 0 );
		int last = lastPlayer();
		int nextPlayerID = ( id + 1 ) % players.size();
		Player nextPlayer = players.get( nextPlayerID );
		if( !Character.isDigit( action ) && action != '-' ) {
			while( nextPlayer.isAllIn() || nextPlayer.isFolded() || nextPlayer.isLost() )
				nextPlayerID = ( nextPlayerID + 1 ) % players.size();
			boolean equal = allEqual();
			if( id == last && equal && ( action == 'H' || action == 'C' ) && round != 8 ) {
				System.out.println( "Turn: " + playerTurn );
				if( message.length() > 1 && message.charAt( 1 ) == 'B' )
					return;
				round++;
				changingCards = true;
				server.sendTurnInfo( nextPlayerID, "C" );
			}
			else {
				server.sendTurnInfo( nextPlayerID, "B" + determineMoves( players.get( id ) ) );
			}
		}
		else {
			int turn = playerTurn;
			if( turn == last ) {
				round++;
				changingCards = false;
				playerTurn = nextPlayerID;
				server.sendTurnInfo( nextPlayerID, "B" + determineMoves( players.get( id ) ) );
			}
			else {
				playerTurn = nextPlayerID;
				server.sendTurnInfo( nextPlayerID, "C" );
			}
		}
	}
}